/***************************************************************************
 *   This is a part of the AMCAX Web CAD.                                  *
 *   Copyright (C) AMCAX, All rights reserved.                             *
 ***************************************************************************/
import { CMEVENTS, FeatureManager } from '@amcax/base';
import { userEvents } from '@app-cad/module';
import pako from 'pako';
import { nmAppErrorLog, nmAppWarnLog } from '../common/utils';
import { IGspEvent } from '../types/document';
import { events } from '../utils';
import WsTunnel from './WsTunnel';

export class GspEngine {
  private static instance: GspEngine;
  private tunnel: WsTunnel = null;
  private connectionPromise: Promise<GspEngine> = null;
  private documentId: string = '';
  private cmdIndex: number = 0;
  private cmdMap: Map<number, any> = new Map();

  private healthCheckCommand = 'HealthCheck';
  private heartbeatTimer: NodeJS.Timeout | null = null;
  private heartbeatInterval = 30000;
  private heartbeatCounter = 0;

  private constructor() {}

  public static getInstance(): GspEngine {
    if (!GspEngine.instance) {
      GspEngine.instance = new GspEngine();
    }
    return GspEngine.instance;
  }

  static async connect(
    documentId: string,
    version?: string | number | null,
    wssUrl?: string,
  ): Promise<GspEngine> {
    const engine = GspEngine.getInstance();
    if (engine.tunnel?.isConnecting) {
      return engine.connectionPromise;
    }
    if (engine.isOpen) {
      return engine;
    }
    engine.connectionPromise = engine.open(documentId, version, wssUrl);
    return engine.connectionPromise;
  }

  private async open(
    documentId: string,
    version?: string | number | null,
    wssUrl?: string,
  ) {
    const usePool = FeatureManager.isFeatureEnabled('ws-pool');

    try {
      const engine = GspEngine.getInstance();
      const token = localStorage.getItem('NEXT_TOKEN');
      const tokenType = localStorage.getItem('NEXT_TOKEN_TYPE');
      const url =
        `/ws/v2/gsp?k=amcax&authorization=${tokenType} ${token}` +
        `&p=${documentId}${version ? '&v=' + version : ''}&pool=${usePool}`;
      engine.tunnel = new WsTunnel(
        'NextGSP',
        wssUrl || engine.createWsUrlWithCurLoc(url),
      );
      await engine.tunnel.open();
      engine.startHeartbeat();

      engine.documentId = documentId;
      engine.cmdIndex = 0;
      engine.cmdMap = new Map();
      engine.tunnel.attach('data', (evt: any) => {
        engine.onMessage(evt);
      });
      engine.tunnel.attach('status', (opened: any) => {
        if (!opened) {
          engine.stop();
        }
        userEvents.emit(CMEVENTS.WS_STATUS_CHANGED, opened);
      });
      return engine;
    } catch (e) {
      nmAppErrorLog('failed to start gsp engine', e);
      alert('failed to start gsp engine');
    }
    return null;
  }

  get isOpen() {
    return this.tunnel?.isOpen;
  }

  async stop() {
    this.stopHeartbeat();

    try {
      this.tunnel.close();
      this.tunnel = null;
      this.documentId = '';
      this.cmdIndex = 0;
    } catch (error) {
      nmAppErrorLog('failed to stop core engine', error);
      throw new Error('Failed to stop core engine');
    }
  }

  execCmd(cmd: any, returnFullResponse = false): Promise<any> {
    if (cmd.c !== this.healthCheckCommand) {
      this.resetHeartbeat();
    }

    cmd.s = this.documentId;
    cmd.i = ++this.cmdIndex;
    cmd.z = true;
    const result = new Promise((resolve, reject) => {
      this.cmdMap.set(this.cmdIndex, { resolve, reject, returnFullResponse });
      this.tunnel.send(JSON.stringify(cmd));
    });
    return result;
  }

  async onMessage(raw: any) {
    let data = raw;
    if (typeof raw !== 'string') {
      data = await this.readCompressedBlob(raw);
    }
    let result: any;
    try {
      result = JSON.parse(data);
    } catch (error) {
      nmAppErrorLog('GspEngine onMessage', {
        data,
        error,
      });

      return;
    }
    if (result.event) {
      // 接收websocket所推数据
      events.emit((result as IGspEvent).event, result as IGspEvent);
      return;
    }
    const solver = this.cmdMap.get(result.index);
    if (solver) {
      this.cmdMap.delete(result.index);
      if (result.code === 0) {
        solver.resolve(solver.returnFullResponse ? result : result.data);
      } else {
        solver.reject({ code: result.code, message: result.message });
        userEvents.emit(CMEVENTS.CMD_FAILED, {
          code: result.code,
          message: result.message,
        });
      }
    } else {
      nmAppWarnLog('未知请求结果', result);
      // 其他错误处理逻辑
    }
  }

  async readCompressedBlob(blob: Blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = function () {
        const data = pako.inflate(reader.result as any);
        resolve(new TextDecoder().decode(data));
      };
      reader.onerror = function (err) {
        reject(err);
      };
      reader.readAsArrayBuffer(blob);
    });
  }

  createWsUrlWithCurLoc(uri: string): string | URL {
    if (process.env.NODE_ENV === 'development') {
      return `wss://${process.env.REACT_APP_NEXT_URL}${uri}`;
      // return `wss://next-dev.amcax.net${uri}`;
    }
    const url = new URL(uri, window.location.href);
    url.protocol = url.protocol.replace('http', 'ws');
    return url;
  }

  private startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      this.heartbeatCounter++;
      if (this.heartbeatCounter > 50) {
        this.stopHeartbeat();
      } else {
        this.execCmd({ c: this.healthCheckCommand });
      }
    }, this.heartbeatInterval);
  }

  private stopHeartbeat() {
    this.heartbeatCounter = 0;
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  private resetHeartbeat() {
    this.stopHeartbeat();
    this.startHeartbeat();
  }
}
